package com.nononsenseapps.feeder.sync.local

import android.content.Context
import androidx.room.withTransaction
import com.gitee.wsl.ext.base.isFalse
import com.gitee.wsl.ext.base.trueValue
import com.nononsenseapps.feeder.db.DEFAULT
import com.nononsenseapps.feeder.db.entity.FeedItem
import com.nononsenseapps.feeder.db.entity.FeedSyncItem
import com.nononsenseapps.feeder.db.room.AppDatabase
import com.nononsenseapps.feeder.db.room.FeedItemDao
import com.nononsenseapps.feeder.db.room.ID_UNSET
import com.nononsenseapps.feeder.db.room.upsert
import com.nononsenseapps.feeder.net.NetManager
import com.nononsenseapps.feeder.parser.FeedParser
import com.nononsenseapps.feeder.processor.ProcessorManager
import com.nononsenseapps.feeder.sync.local.source.dir.DirSource
import com.nononsenseapps.feeder.sync.local.source.email.EmailSource_K9
import com.nononsenseapps.feeder.sync.local.source.sms.SmsSource
import com.nononsenseapps.feeder.sync.local.source.url.URLSource
import com.nononsenseapps.feeder.util.PrefUtils
import com.nononsenseapps.feeder.util.SyncState
import com.nononsenseapps.feeder.util.feedParser
import com.nononsenseapps.feeder.util.isTrue
import com.nononsenseapps.feeder.util.sloppyLinkToStrictURL
import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLNoThrows
import com.nononsenseapps.feeder.util.unfold
import com.nononsenseapps.feeder.worker.requestFeedFullPageSync
import com.nononsenseapps.feeder.worker.requestFeedItemPreLoader
import com.nononsenseapps.feeder.parser.Feed
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import okhttp3.Response
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import timber.log.Timber
import java.net.SocketTimeoutException
import java.net.URL
import java.util.concurrent.Semaphore
import kotlin.math.max
import kotlin.system.measureTimeMillis

suspend fun syncFeeds(context: Context,
                      feedId: Long = ID_UNSET,
                      feedTag: String = "",
                      forceNetwork: Boolean = false,
                      parallel: Boolean = false,
                      force:Boolean = false,
                      minFeedAgeMinutes: Int = 15): SyncState =
        syncFeeds(
                 context,
                db = AppDatabase.getInstance(context),
                feedParser = context.feedParser,
                feedId = feedId,
                feedTag = feedTag,
                maxFeedItemCount = PrefUtils.maximumItemCountPerFeed(context),
                forceNetwork = forceNetwork,
                force = force,
                parallel = parallel,
                minFeedAgeMinutes = minFeedAgeMinutes
        )

internal suspend fun syncFeeds(context: Context,
                               db: AppDatabase,
                               feedParser: FeedParser,
                               feedId: Long = ID_UNSET,
                               feedTag: String = "",
                               maxFeedItemCount: Int = 100,
                               forceNetwork: Boolean = false,
                               parallel: Boolean = false,
                               force:Boolean = false,
                               minFeedAgeMinutes: Int = 15): SyncState {
    val syncState=SyncState(feedId,feedTag,"",success = false)
    val time = measureTimeMillis {
        try {
            supervisorScope {
                val staleTime: Long = if (forceNetwork) {
                    DateTime.now(DateTimeZone.UTC).millis
                } else {
                    DateTime.now(DateTimeZone.UTC)
                            .minusMinutes(minFeedAgeMinutes)
                            .millis
                }
                val feedsToFetch = if(force)
                                        feedsToSync(db, feedId, feedTag)
                                   else
                                        feedsToSync(db, feedId, feedTag, staleTime = staleTime).filter { it.lastSync.plusMinutes(it.sync_period)<=DateTime.now(DateTimeZone.UTC) }

                Timber.d( "syncFeeds Syncing ${feedsToFetch.size} feeds")

                val coroutineContext = when (parallel) {
                    true -> Dispatchers.Default
                    false -> this.coroutineContext
                }+ CoroutineExceptionHandler { _, throwable ->
                   Timber.e( "Error during sync: ${throwable.message}")
                }
                val timeOutUrlList= mutableSetOf<String>()
                val feedDao=db.feedDao()
                val feedSyncItemDao=db.feedSyncItemDao()
                var fullPageLoadMust=false
                var preLoader=false
                val barrier:Semaphore? = (feedsToFetch.size > 6).trueValue(Semaphore(5))
                feedsToFetch.forEachIndexed { index, feed ->
                    if(feed.id> ID_UNSET) {
                        if(index > 1) {
                            timeOutUrlList.contains(feed.url.host).isTrue {
                                Timber.d("find time out feed:${feed.url}")
                                feed.syncSuccess=false
                                launch(coroutineContext) {
                                    feedDao.updateFeed(feed)
                                }
                                return@forEachIndexed
                            }
                            if(index % 5 == 1)
                                delay(3000)
                        }
                        fullPageLoadMust = fullPageLoadMust || feed.fullTextSync
                        preLoader = preLoader || feed.imagePreDownload || feed.preDownload
                        launch(coroutineContext) {
                            withContext(Dispatchers.IO) {
                                barrier?.acquire()

                                val feedSyncItem = FeedSyncItem()
                                val startTime = System.currentTimeMillis()
                                syncFeed(
                                    feed,
                                    ProcessorManager.default,
                                    db = db,
                                    feedParser = feedParser,
                                    maxFeedItemCount = maxFeedItemCount,
                                    forceNetwork = forceNetwork,
                                    force = force,
                                    syncState = syncState,
                                    feedSyncItem = feedSyncItem,
                                    context = context
                                ).let {
                                    feedSyncItem.feedId = feed.id
                                    feedSyncItem.syncTimeInterval =
                                        System.currentTimeMillis() - startTime
                                    feedSyncItemDao.insert(feedSyncItem)
                                    feedSyncItem.syncSuccess.isFalse { timeOutUrlList.add(feed.url.host) }
                                }
                                barrier?.release()
                            }
                        }
                    }
                }
                if(fullPageLoadMust)
                    requestFeedFullPageSync(context)
                if(preLoader)
                    requestFeedItemPreLoader(context)
                syncState.success = true
            }
        } catch (e: Throwable) {
            Timber.e(e.localizedMessage)
        }
    }
    Timber.d(  "syncFeeds id:$feedId,tag:$feedTag\trun Completed in $time ms")
    return syncState
}


private suspend fun syncFeed(feedSql: com.nononsenseapps.feeder.db.entity.Feed,
                             processorManager: ProcessorManager,
                             db: AppDatabase,
                             feedParser: FeedParser,
                             maxFeedItemCount: Int,
                             forceNetwork: Boolean = false,
                             syncState: SyncState?=null,
                             force:Boolean = false,
                             feedSyncItem: FeedSyncItem,
                             context: Context
) {
    val feedDao=db.feedDao()
    val itemDao = db.feedItemDao()
    try {
        val feedSource: Feed? =if(DEFAULT.SOURCE_HOST == feedSql.url.host) {
                when(feedSql.url.path){
                    SmsSource.SMS_PATH-> {
                        val phoneNumber = feedSql.url.query
                        SmsSource.fetchFeedForSms(feedSql,phoneNumber,context)
                    }
                    EmailSource_K9.EMAIL_K9_PATH->{
                        val subject = feedSql.url.query
                        EmailSource_K9.fetchFeedForEmailK9(feedSql,subject,context)
                    }
                    DirSource.DIR_PATH ->{
                        val dirPath = feedSql.url.query
                        DirSource.fetchFeedForDir(feedSql,context,dirPath)
                    }
                    else -> null
                }
        }else
                 URLSource.fetchFeedForUrl(feedSql,feedParser,forceNetwork=forceNetwork)

        // Always update the feeds last sync field
        feedSql.lastSync = DateTime.now(DateTimeZone.UTC)

        if (feedSource == null) {
            feedDao.upsert(feedSql)
        } else {
            val idCount = feedSource.items?.map { it.id ?: 0 }?.toSet()?.size
            var newCount = 0
            val itemIds = when (idCount == feedSource.items?.size) {
                true -> {
                    feedSource.items?.map { it.id ?: "should not be possible" }
                }
                false -> {
                    feedSource.items?.map {
                        "${it.title}-${it.summary}"
                    }
                }
            } ?: emptyList()

            if(!force) {
                db.withTransaction {
                    syncFeedItems(feedSource, itemIds, feedSql, itemDao).let {
                        newCount += it.size
                        coroutineScope {
                            it.forEach {
                                processorManager.dispatch(feedSql, it, context, db)
                            }
                        }
                    }
                }
            }else{
                syncFeedItems(feedSource, itemIds, feedSql, itemDao).let {
                    newCount += it.size
                    coroutineScope {
                        it.forEach {
                            processorManager.dispatch(feedSql, it, context, db)
                        }
                    }
                }
            }

            // Update feed last so lastsync is only set after all items have been handled
            // for the rare case that the job is cancelled prematurely
            feedSql.responseHash = feedSource.responseHash
            feedSql.title = feedSource.title ?: feedSql.title
            feedSql.url = feedSource.feed_url?.let { sloppyLinkToStrictURLNoThrows(it) } ?: feedSql.url
            feedSql.imageUrl = feedSource.icon?.let { sloppyLinkToStrictURLNoThrows(it) } ?: feedSql.imageUrl
            feedSql.syncNewItemCount+=newCount
            feedSql.syncSuccess=true
            feedSql.lastSuccessSyncTime = DateTime.now(DateTimeZone.UTC)
            feedDao.upsert(feedSql)
            feedSyncItem.syncNewItemCount=newCount
            // Finally, prune database of old items
            val maxCount= max(maxFeedItemCount, feedSource.items?.size ?: 0)
            if( maxCount<101 || itemDao.count(feedSql.id)>maxCount ) {
                if (feedSql.backup)
                    itemDao.backupItems(feedSql.id, keepCount = maxCount)
                else {
                    itemDao.backupReadItems(feedSql.id,keepCount = maxCount)
                    itemDao.cleanItemsInFeed(feedSql.id, maxCount)
                }
            }
            syncState?.apply {
                appSuccess(feedSql.title+"+("+newCount+")")
                count += newCount
                feedSql.notify.isTrue { notify=true }
            }
        }
    } catch (e: ResponseFailure) {
        Timber.e( "Failed to fetch ${feedSql.displayTitle}: ${e.message}")
        if(feedSql.id > ID_UNSET) {
            feedSql.netErrCount = feedSql.netErrCount + 1
            feedSql.syncSuccess=false
            feedSql.lastSync = DateTime.now(DateTimeZone.UTC)
            feedDao.upsert(feedSql)
            feedSyncItem.syncSuccess = false
        }
        syncState?.appError(feedSql.title+"\te-"+feedSql.netErrCount)
    } catch (t: Throwable) {
        Timber.e(  "sync ${feedSql.title} Something went wrong: ${t.printStackTrace()}")
        if(feedSql.id > ID_UNSET) {
            feedSql.netErrCount = feedSql.netErrCount + 1
            feedSql.syncSuccess=false
            feedSql.lastSync = DateTime.now(DateTimeZone.UTC)
            feedDao.upsert(feedSql)
            feedSyncItem.syncSuccess = false
        }
        syncState?.appError(feedSql.title+"\tt-"+feedSql.netErrCount)
    }
}

private fun syncFeedItems(feedSource: Feed, itemIds:List<String>, feedSql: com.nononsenseapps.feeder.db.entity.Feed, itemDao:FeedItemDao):List<FeedItem>{
    var feedItemSql: FeedItem?
    var itemId:Long?
    val newFeedItems= mutableListOf<FeedItem>()
    feedSource.items?.zip(itemIds)
        ?.reversed()
        ?.forEach { (item, id) ->
            itemId = itemDao.isExistWithGuid(guid = id)
            feedItemSql = null
            if (itemId != null) {
                if (feedSql.overWrite) {
                    itemDao.loadFeedItem(itemId!!)?.let {
                        feedItemSql = it
                        item.unfold.isTrue { item.unfold() }
                        it.updateFromParsedEntry(item, feedSource)
                        it.unread = true
                        it.fullTextSync = feedSql.fullTextSync
                        itemDao.updateFeedItem(it)
                    }
                }
            } else {
                feedItemSql = FeedItem(fullTextSync = feedSql.fullTextSync)
                item.id = id
                item.unfold.isTrue { item.unfold() }
                feedItemSql!!.updateFromParsedEntry(item, feedSource)
                feedItemSql!!.feedId = feedSql.id
                newFeedItems.add(feedItemSql!!)
                feedItemSql!!.id = itemDao.insertFeedItem(feedItemSql!!)
            }
        }
    return newFeedItems
}

suspend fun fetchFeed(
    feedParser: FeedParser,
    feed: com.nononsenseapps.feeder.db.entity.Feed,
    forceNetwork: Boolean = false
): Response {
    return if(feed.mutableServerFeed)
             fetchFeed(feedParser,feed,forceNetwork, NetManager.mutableServerList)
          else
             fetchFeed(feedParser, feed.url, forceNetwork)
}

private suspend fun fetchFeed(
    feedParser: FeedParser,
    feed: com.nononsenseapps.feeder.db.entity.Feed,
    forceNetwork: Boolean = false,
    mutableServer:List<String> ,
    index:Int = -1
): Response {
    if(mutableServer.isEmpty()) return fetchFeed(feedParser, feed.url, forceNetwork)

    if( index >= mutableServer.size ) throw Exception("mutableServer fetch index over ...")

    val nextIndex = index+1

    try {

       val feedUrlString = feed.url.toString()

       val url = if(index == -1) feedUrlString else{
           val feedBaseHost = mutableServer.find { feedUrlString.startsWith(it) }
           val newServerHost = mutableServer[index]
           if( feedBaseHost != null) {
               if(feedBaseHost == newServerHost)
                   return fetchFeed(feedParser , feed , forceNetwork , mutableServer, nextIndex)
               feedUrlString.replace(feedBaseHost,newServerHost)
           }else
               throw Exception("mutableServer base url find false ..")
       }

       feedParser.getResponse(sloppyLinkToStrictURL(url), forceNetwork = forceNetwork).let{
           if (it.isSuccessful)
               return it
       }

       return fetchFeed(feedParser, feed, forceNetwork, mutableServer, nextIndex)
    } catch (e: ResponseFailure) {
        return fetchFeed(feedParser, feed, forceNetwork, mutableServer, nextIndex)
    } catch (e: SocketTimeoutException) {
        return fetchFeed(feedParser, feed, forceNetwork, mutableServer, nextIndex)
    }
}

private suspend fun fetchFeed(
    feedParser: FeedParser,
    feedUrl: URL,
    forceNetwork: Boolean = false
): Response =
        feedParser.getResponse(feedUrl, forceNetwork = forceNetwork)

internal fun feedsToSync(db: AppDatabase, feedId: Long, tag: String, staleTime: Long = -1L): List<com.nononsenseapps.feeder.db.entity.Feed> {
    return when {
        feedId > 0 -> {
            val feed = if (staleTime > 0) db.feedDao().loadFeedIfStale(feedId, staleTime = staleTime) else db.feedDao().loadFeed(feedId)
            if (feed != null) {
                listOf(feed)
            } else {
                emptyList()
            }
        }
        !tag.isEmpty() -> if (staleTime > 0) db.feedDao().loadFeedsIfStale(tag = tag, staleTime = staleTime) else db.feedDao().loadFeedsForSync(tag)
        else -> if (staleTime > 0) db.feedDao().loadFeedsIfStale(staleTime) else db.feedDao().loadFeedsForSync()
    }
}

class ResponseFailure(message: String?) : Exception(message)
